/*
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at 
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *  
 */

using System;
using java = biz.ritter.javapi;
using org.apache.commons.collections;
using org.apache.commons.collections.keyvalue;

namespace org.apache.commons.collections.map
{

    /**
     * A <code>Map</code> implementation that uses multiple keys to map the value.
     * <p>
     * This class is the most efficient way to uses multiple keys to map to a value.
     * The best way to use this class is via the additional map-style methods.
     * These provide <code>get</code>, <code>containsKey</code>, <code>put</code> and
     * <code>remove</code> for individual keys which operate without extra object creation.
     * <p>
     * The additional methods are the main interface of this map.
     * As such, you will not normally hold this map in a variable of type <code>Map</code>.
     * <p>
     * The normal map methods take in and return a {@link MultiKey}.
     * If you try to use <code>put()</code> with any other object type a
     * <code>ClassCastException</code> is thrown. If you try to use <code>null</code> as
     * the key in <code>put()</code> a <code>NullPointerException</code> is thrown.
     * <p>
     * This map is implemented as a decorator of a <code>AbstractHashedMap</code> which
     * enables extra behaviour to be added easily.
     * <ul>
     * <li><code>MultiKeyMap.decorate(new LinkedMap())</code> creates an ordered map.
     * <li><code>MultiKeyMap.decorate(new LRUMap())</code> creates an least recently used map.
     * <li><code>MultiKeyMap.decorate(new ReferenceMap())</code> creates a garbage collector sensitive map.
     * </ul>
     * Note that <code>IdentityMap</code> and <code>ReferenceIdentityMap</code> are unsuitable
     * for use as the key comparison would work on the whole MultiKey, not the elements within.
     * <p>
     * As an example, consider a least recently used cache that uses a String airline code
     * and a Locale to lookup the airline's name:
     * <pre>
     * private MultiKeyMap cache = MultiKeyMap.decorate(new LRUMap(50));
     * 
     * public String getAirlineName(String code, String locale) {
     *   String name = (String) cache.get(code, locale);
     *   if (name == null) {
     *     name = getAirlineNameFromDB(code, locale);
     *     cache.put(code, locale, name);
     *   }
     *   return name;
     * }
     * </pre>
     * <p>
     * <strong>Note that MultiKeyMap is not synchronized and is not thread-safe.</strong>
     * If you wish to use this map from multiple threads concurrently, you must use
     * appropriate synchronization. This class may throw exceptions when accessed
     * by concurrent threads without synchronization.
     *
     * @since Commons Collections 3.1
     * @version $Revision$ $Date$
     *
     * @author Stephen Colebourne
     */
    [Serializable]
    public class MultiKeyMap
            : IterableMap, java.io.Serializable
    {

        /** Serialisation version */
        private static readonly long serialVersionUID = -1788199231038721040L;

        /** The decorated map */
        protected readonly AbstractHashedMap map;

        //-----------------------------------------------------------------------
        /**
         * Decorates the specified map to add the MultiKeyMap API and fast query.
         * The map must not be null and must be empty.
         *
         * @param map  the map to decorate, not null
         * @throws IllegalArgumentException if the map is null or not empty
         */
        public static MultiKeyMap decorate(AbstractHashedMap map)
        {
            if (map == null)
            {
                throw new java.lang.IllegalArgumentException("Map must not be null");
            }
            if (map.size() > 0)
            {
                throw new java.lang.IllegalArgumentException("Map must be empty");
            }
            return new MultiKeyMap(map);
        }

        //-----------------------------------------------------------------------    
        /**
         * Constructs a new MultiKeyMap that decorates a <code>HashedMap</code>.
         */
        public MultiKeyMap()
            : base()
        {
            map = new HashedMap();
        }

        /**
         * Constructor that decorates the specified map and is called from
         * {@link #decorate(AbstractHashedMap)}.
         * The map must not be null and should be empty or only contain valid keys.
         * This constructor performs no validation.
         *
         * @param map  the map to decorate
         */
        protected MultiKeyMap(AbstractHashedMap map)
            : base()
        {
            this.map = map;
        }

        //-----------------------------------------------------------------------
        /**
         * Gets the value mapped to the specified multi-key.
         * 
         * @param key1  the first key
         * @param key2  the second key
         * @return the mapped value, null if no match
         */
        public Object get(Object key1, Object key2)
        {
            int hashCode = hash(key1, key2);
            AbstractHashedMap.HashEntry entry = map.data[map.hashIndex(hashCode, map.data.Length)];
            while (entry != null)
            {
                if (entry.hashCode == hashCode && isEqualKey(entry, key1, key2))
                {
                    return entry.getValue();
                }
                entry = entry.nextJ;
            }
            return null;
        }

        /**
         * Checks whether the map contains the specified multi-key.
         * 
         * @param key1  the first key
         * @param key2  the second key
         * @return true if the map contains the key
         */
        public bool containsKey(Object key1, Object key2)
        {
            int hashCode = hash(key1, key2);
            AbstractHashedMap.HashEntry entry = map.data[map.hashIndex(hashCode, map.data.Length)];
            while (entry != null)
            {
                if (entry.hashCode == hashCode && isEqualKey(entry, key1, key2))
                {
                    return true;
                }
                entry = entry.nextJ;
            }
            return false;
        }

        /**
         * Stores the value against the specified multi-key.
         * 
         * @param key1  the first key
         * @param key2  the second key
         * @param value  the value to store
         * @return the value previously mapped to this combined key, null if none
         */
        public Object put(Object key1, Object key2, Object value)
        {
            int hashCode = hash(key1, key2);
            int index = map.hashIndex(hashCode, map.data.Length);
            AbstractHashedMap.HashEntry entry = map.data[index];
            while (entry != null)
            {
                if (entry.hashCode == hashCode && isEqualKey(entry, key1, key2))
                {
                    Object oldValue = entry.getValue();
                    map.updateEntry(entry, value);
                    return oldValue;
                }
                entry = entry.nextJ;
            }

            map.addMapping(index, hashCode, new MultiKey(key1, key2), value);
            return null;
        }

        /**
         * Removes the specified multi-key from this map.
         * 
         * @param key1  the first key
         * @param key2  the second key
         * @return the value mapped to the removed key, null if key not in map
         */
        public Object remove(Object key1, Object key2)
        {
            int hashCode = hash(key1, key2);
            int index = map.hashIndex(hashCode, map.data.Length);
            AbstractHashedMap.HashEntry entry = map.data[index];
            AbstractHashedMap.HashEntry previous = null;
            while (entry != null)
            {
                if (entry.hashCode == hashCode && isEqualKey(entry, key1, key2))
                {
                    Object oldValue = entry.getValue();
                    map.removeMapping(entry, index, previous);
                    return oldValue;
                }
                previous = entry;
                entry = entry.nextJ;
            }
            return null;
        }

        /**
         * Gets the hash code for the specified multi-key.
         * 
         * @param key1  the first key
         * @param key2  the second key
         * @return the hash code
         */
        protected int hash(Object key1, Object key2)
        {
            int h = 0;
            if (key1 != null)
            {
                h ^= key1.GetHashCode();
            }
            if (key2 != null)
            {
                h ^= key2.GetHashCode();
            }
            h += ~(h << 9);
            h ^= java.dotnet.lang.Operator.shiftRightUnsignet(h, 14);
            h += (h << 4);
            h ^= java.dotnet.lang.Operator.shiftRightUnsignet(h, 10);
            return h;
        }

        /**
         * Is the key equal to the combined key.
         * 
         * @param entry  the entry to compare to
         * @param key1  the first key
         * @param key2  the second key
         * @return true if the key matches
         */
        protected bool isEqualKey(AbstractHashedMap.HashEntry entry, Object key1, Object key2)
        {
            MultiKey multi = (MultiKey)entry.getKey();
            return
                multi.size() == 2 &&
                (key1 == null ? multi.getKey(0) == null : key1.equals(multi.getKey(0))) &&
                (key2 == null ? multi.getKey(1) == null : key2.equals(multi.getKey(1)));
        }

        //-----------------------------------------------------------------------
        /**
         * Gets the value mapped to the specified multi-key.
         * 
         * @param key1  the first key
         * @param key2  the second key
         * @param key3  the third key
         * @return the mapped value, null if no match
         */
        public Object get(Object key1, Object key2, Object key3)
        {
            int hashCode = hash(key1, key2, key3);
            AbstractHashedMap.HashEntry entry = map.data[map.hashIndex(hashCode, map.data.Length)];
            while (entry != null)
            {
                if (entry.hashCode == hashCode && isEqualKey(entry, key1, key2, key3))
                {
                    return entry.getValue();
                }
                entry = entry.nextJ;
            }
            return null;
        }

        /**
         * Checks whether the map contains the specified multi-key.
         * 
         * @param key1  the first key
         * @param key2  the second key
         * @param key3  the third key
         * @return true if the map contains the key
         */
        public bool containsKey(Object key1, Object key2, Object key3)
        {
            int hashCode = hash(key1, key2, key3);
            AbstractHashedMap.HashEntry entry = map.data[map.hashIndex(hashCode, map.data.Length)];
            while (entry != null)
            {
                if (entry.hashCode == hashCode && isEqualKey(entry, key1, key2, key3))
                {
                    return true;
                }
                entry = entry.nextJ;
            }
            return false;
        }

        /**
         * Stores the value against the specified multi-key.
         * 
         * @param key1  the first key
         * @param key2  the second key
         * @param key3  the third key
         * @param value  the value to store
         * @return the value previously mapped to this combined key, null if none
         */
        public Object put(Object key1, Object key2, Object key3, Object value)
        {
            int hashCode = hash(key1, key2, key3);
            int index = map.hashIndex(hashCode, map.data.Length);
            AbstractHashedMap.HashEntry entry = map.data[index];
            while (entry != null)
            {
                if (entry.hashCode == hashCode && isEqualKey(entry, key1, key2, key3))
                {
                    Object oldValue = entry.getValue();
                    map.updateEntry(entry, value);
                    return oldValue;
                }
                entry = entry.nextJ;
            }

            map.addMapping(index, hashCode, new MultiKey(key1, key2, key3), value);
            return null;
        }

        /**
         * Removes the specified multi-key from this map.
         * 
         * @param key1  the first key
         * @param key2  the second key
         * @param key3  the third key
         * @return the value mapped to the removed key, null if key not in map
         */
        public Object remove(Object key1, Object key2, Object key3)
        {
            int hashCode = hash(key1, key2, key3);
            int index = map.hashIndex(hashCode, map.data.Length);
            AbstractHashedMap.HashEntry entry = map.data[index];
            AbstractHashedMap.HashEntry previous = null;
            while (entry != null)
            {
                if (entry.hashCode == hashCode && isEqualKey(entry, key1, key2, key3))
                {
                    Object oldValue = entry.getValue();
                    map.removeMapping(entry, index, previous);
                    return oldValue;
                }
                previous = entry;
                entry = entry.nextJ;
            }
            return null;
        }

        /**
         * Gets the hash code for the specified multi-key.
         * 
         * @param key1  the first key
         * @param key2  the second key
         * @param key3  the third key
         * @return the hash code
         */
        protected int hash(Object key1, Object key2, Object key3)
        {
            int h = 0;
            if (key1 != null)
            {
                h ^= key1.GetHashCode();
            }
            if (key2 != null)
            {
                h ^= key2.GetHashCode();
            }
            if (key3 != null)
            {
                h ^= key3.GetHashCode();
            }
            h += ~(h << 9);
            h ^= java.dotnet.lang.Operator.shiftRightUnsignet(h, 14);
            h += (h << 4);
            h ^= java.dotnet.lang.Operator.shiftRightUnsignet(h, 10);
            return h;
        }

        /**
         * Is the key equal to the combined key.
         * 
         * @param entry  the entry to compare to
         * @param key1  the first key
         * @param key2  the second key
         * @param key3  the third key
         * @return true if the key matches
         */
        protected bool isEqualKey(AbstractHashedMap.HashEntry entry, Object key1, Object key2, Object key3)
        {
            MultiKey multi = (MultiKey)entry.getKey();
            return
                multi.size() == 3 &&
                (key1 == null ? multi.getKey(0) == null : key1.equals(multi.getKey(0))) &&
                (key2 == null ? multi.getKey(1) == null : key2.equals(multi.getKey(1))) &&
                (key3 == null ? multi.getKey(2) == null : key3.equals(multi.getKey(2)));
        }

        //-----------------------------------------------------------------------
        /**
         * Gets the value mapped to the specified multi-key.
         * 
         * @param key1  the first key
         * @param key2  the second key
         * @param key3  the third key
         * @param key4  the fourth key
         * @return the mapped value, null if no match
         */
        public Object get(Object key1, Object key2, Object key3, Object key4)
        {
            int hashCode = hash(key1, key2, key3, key4);
            AbstractHashedMap.HashEntry entry = map.data[map.hashIndex(hashCode, map.data.Length)];
            while (entry != null)
            {
                if (entry.hashCode == hashCode && isEqualKey(entry, key1, key2, key3, key4))
                {
                    return entry.getValue();
                }
                entry = entry.nextJ;
            }
            return null;
        }

        /**
         * Checks whether the map contains the specified multi-key.
         * 
         * @param key1  the first key
         * @param key2  the second key
         * @param key3  the third key
         * @param key4  the fourth key
         * @return true if the map contains the key
         */
        public bool containsKey(Object key1, Object key2, Object key3, Object key4)
        {
            int hashCode = hash(key1, key2, key3, key4);
            AbstractHashedMap.HashEntry entry = map.data[map.hashIndex(hashCode, map.data.Length)];
            while (entry != null)
            {
                if (entry.hashCode == hashCode && isEqualKey(entry, key1, key2, key3, key4))
                {
                    return true;
                }
                entry = entry.nextJ;
            }
            return false;
        }

        /**
         * Stores the value against the specified multi-key.
         * 
         * @param key1  the first key
         * @param key2  the second key
         * @param key3  the third key
         * @param key4  the fourth key
         * @param value  the value to store
         * @return the value previously mapped to this combined key, null if none
         */
        public Object put(Object key1, Object key2, Object key3, Object key4, Object value)
        {
            int hashCode = hash(key1, key2, key3, key4);
            int index = map.hashIndex(hashCode, map.data.Length);
            AbstractHashedMap.HashEntry entry = map.data[index];
            while (entry != null)
            {
                if (entry.hashCode == hashCode && isEqualKey(entry, key1, key2, key3, key4))
                {
                    Object oldValue = entry.getValue();
                    map.updateEntry(entry, value);
                    return oldValue;
                }
                entry = entry.nextJ;
            }

            map.addMapping(index, hashCode, new MultiKey(key1, key2, key3, key4), value);
            return null;
        }

        /**
         * Removes the specified multi-key from this map.
         * 
         * @param key1  the first key
         * @param key2  the second key
         * @param key3  the third key
         * @param key4  the fourth key
         * @return the value mapped to the removed key, null if key not in map
         */
        public Object remove(Object key1, Object key2, Object key3, Object key4)
        {
            int hashCode = hash(key1, key2, key3, key4);
            int index = map.hashIndex(hashCode, map.data.Length);
            AbstractHashedMap.HashEntry entry = map.data[index];
            AbstractHashedMap.HashEntry previous = null;
            while (entry != null)
            {
                if (entry.hashCode == hashCode && isEqualKey(entry, key1, key2, key3, key4))
                {
                    Object oldValue = entry.getValue();
                    map.removeMapping(entry, index, previous);
                    return oldValue;
                }
                previous = entry;
                entry = entry.nextJ;
            }
            return null;
        }

        /**
         * Gets the hash code for the specified multi-key.
         * 
         * @param key1  the first key
         * @param key2  the second key
         * @param key3  the third key
         * @param key4  the fourth key
         * @return the hash code
         */
        protected int hash(Object key1, Object key2, Object key3, Object key4)
        {
            int h = 0;
            if (key1 != null)
            {
                h ^= key1.GetHashCode();
            }
            if (key2 != null)
            {
                h ^= key2.GetHashCode();
            }
            if (key3 != null)
            {
                h ^= key3.GetHashCode();
            }
            if (key4 != null)
            {
                h ^= key4.GetHashCode();
            }
            h += ~(h << 9);
            h ^= java.dotnet.lang.Operator.shiftRightUnsignet(h, 14);
            h += (h << 4);
            h ^= java.dotnet.lang.Operator.shiftRightUnsignet(h, 10);
            return h;
        }

        /**
         * Is the key equal to the combined key.
         * 
         * @param entry  the entry to compare to
         * @param key1  the first key
         * @param key2  the second key
         * @param key3  the third key
         * @param key4  the fourth key
         * @return true if the key matches
         */
        protected bool isEqualKey(AbstractHashedMap.HashEntry entry, Object key1, Object key2, Object key3, Object key4)
        {
            MultiKey multi = (MultiKey)entry.getKey();
            return
                multi.size() == 4 &&
                (key1 == null ? multi.getKey(0) == null : key1.equals(multi.getKey(0))) &&
                (key2 == null ? multi.getKey(1) == null : key2.equals(multi.getKey(1))) &&
                (key3 == null ? multi.getKey(2) == null : key3.equals(multi.getKey(2))) &&
                (key4 == null ? multi.getKey(3) == null : key4.equals(multi.getKey(3)));
        }

        //-----------------------------------------------------------------------
        /**
         * Gets the value mapped to the specified multi-key.
         * 
         * @param key1  the first key
         * @param key2  the second key
         * @param key3  the third key
         * @param key4  the fourth key
         * @param key5  the fifth key
         * @return the mapped value, null if no match
         */
        public Object get(Object key1, Object key2, Object key3, Object key4, Object key5)
        {
            int hashCode = hash(key1, key2, key3, key4, key5);
            AbstractHashedMap.HashEntry entry = map.data[map.hashIndex(hashCode, map.data.Length)];
            while (entry != null)
            {
                if (entry.hashCode == hashCode && isEqualKey(entry, key1, key2, key3, key4, key5))
                {
                    return entry.getValue();
                }
                entry = entry.nextJ;
            }
            return null;
        }

        /**
         * Checks whether the map contains the specified multi-key.
         * 
         * @param key1  the first key
         * @param key2  the second key
         * @param key3  the third key
         * @param key4  the fourth key
         * @param key5  the fifth key
         * @return true if the map contains the key
         */
        public bool containsKey(Object key1, Object key2, Object key3, Object key4, Object key5)
        {
            int hashCode = hash(key1, key2, key3, key4, key5);
            AbstractHashedMap.HashEntry entry = map.data[map.hashIndex(hashCode, map.data.Length)];
            while (entry != null)
            {
                if (entry.hashCode == hashCode && isEqualKey(entry, key1, key2, key3, key4, key5))
                {
                    return true;
                }
                entry = entry.nextJ;
            }
            return false;
        }

        /**
         * Stores the value against the specified multi-key.
         * 
         * @param key1  the first key
         * @param key2  the second key
         * @param key3  the third key
         * @param key4  the fourth key
         * @param key5  the fifth key
         * @param value  the value to store
         * @return the value previously mapped to this combined key, null if none
         */
        public Object put(Object key1, Object key2, Object key3, Object key4, Object key5, Object value)
        {
            int hashCode = hash(key1, key2, key3, key4, key5);
            int index = map.hashIndex(hashCode, map.data.Length);
            AbstractHashedMap.HashEntry entry = map.data[index];
            while (entry != null)
            {
                if (entry.hashCode == hashCode && isEqualKey(entry, key1, key2, key3, key4, key5))
                {
                    Object oldValue = entry.getValue();
                    map.updateEntry(entry, value);
                    return oldValue;
                }
                entry = entry.nextJ;
            }

            map.addMapping(index, hashCode, new MultiKey(key1, key2, key3, key4, key5), value);
            return null;
        }

        /**
         * Removes the specified multi-key from this map.
         * 
         * @param key1  the first key
         * @param key2  the second key
         * @param key3  the third key
         * @param key4  the fourth key
         * @param key5  the fifth key
         * @return the value mapped to the removed key, null if key not in map
         */
        public Object remove(Object key1, Object key2, Object key3, Object key4, Object key5)
        {
            int hashCode = hash(key1, key2, key3, key4, key5);
            int index = map.hashIndex(hashCode, map.data.Length);
            AbstractHashedMap.HashEntry entry = map.data[index];
            AbstractHashedMap.HashEntry previous = null;
            while (entry != null)
            {
                if (entry.hashCode == hashCode && isEqualKey(entry, key1, key2, key3, key4, key5))
                {
                    Object oldValue = entry.getValue();
                    map.removeMapping(entry, index, previous);
                    return oldValue;
                }
                previous = entry;
                entry = entry.nextJ;
            }
            return null;
        }

        /**
         * Gets the hash code for the specified multi-key.
         * 
         * @param key1  the first key
         * @param key2  the second key
         * @param key3  the third key
         * @param key4  the fourth key
         * @param key5  the fifth key
         * @return the hash code
         */
        protected int hash(Object key1, Object key2, Object key3, Object key4, Object key5)
        {
            int h = 0;
            if (key1 != null)
            {
                h ^= key1.GetHashCode();
            }
            if (key2 != null)
            {
                h ^= key2.GetHashCode();
            }
            if (key3 != null)
            {
                h ^= key3.GetHashCode();
            }
            if (key4 != null)
            {
                h ^= key4.GetHashCode();
            }
            if (key5 != null)
            {
                h ^= key5.GetHashCode();
            }
            h += ~(h << 9);
            h ^= java.dotnet.lang.Operator.shiftRightUnsignet(h, 14);
            h += (h << 4);
            h ^= java.dotnet.lang.Operator.shiftRightUnsignet(h, 10);
            return h;
        }

        /**
         * Is the key equal to the combined key.
         * 
         * @param entry  the entry to compare to
         * @param key1  the first key
         * @param key2  the second key
         * @param key3  the third key
         * @param key4  the fourth key
         * @param key5  the fifth key
         * @return true if the key matches
         */
        protected bool isEqualKey(AbstractHashedMap.HashEntry entry, Object key1, Object key2, Object key3, Object key4, Object key5)
        {
            MultiKey multi = (MultiKey)entry.getKey();
            return
                multi.size() == 5 &&
                (key1 == null ? multi.getKey(0) == null : key1.equals(multi.getKey(0))) &&
                (key2 == null ? multi.getKey(1) == null : key2.equals(multi.getKey(1))) &&
                (key3 == null ? multi.getKey(2) == null : key3.equals(multi.getKey(2))) &&
                (key4 == null ? multi.getKey(3) == null : key4.equals(multi.getKey(3))) &&
                (key5 == null ? multi.getKey(4) == null : key5.equals(multi.getKey(4)));
        }

        //-----------------------------------------------------------------------
        /**
         * Removes all mappings where the first key is that specified.
         * <p>
         * This method removes all the mappings where the <code>MultiKey</code>
         * has one or more keys, and the first matches that specified.
         * 
         * @param key1  the first key
         * @return true if any elements were removed
         */
        public bool removeAll(Object key1)
        {
            bool modified = false;
            MapIterator it = mapIterator();
            while (it.hasNext())
            {
                MultiKey multi = (MultiKey)it.next();
                if (multi.size() >= 1 &&
                    (key1 == null ? multi.getKey(0) == null : key1.equals(multi.getKey(0))))
                {
                    it.remove();
                    modified = true;
                }
            }
            return modified;
        }

        /**
         * Removes all mappings where the first two keys are those specified.
         * <p>
         * This method removes all the mappings where the <code>MultiKey</code>
         * has two or more keys, and the first two match those specified.
         * 
         * @param key1  the first key
         * @param key2  the second key
         * @return true if any elements were removed
         */
        public bool removeAll(Object key1, Object key2)
        {
            bool modified = false;
            MapIterator it = mapIterator();
            while (it.hasNext())
            {
                MultiKey multi = (MultiKey)it.next();
                if (multi.size() >= 2 &&
                    (key1 == null ? multi.getKey(0) == null : key1.equals(multi.getKey(0))) &&
                    (key2 == null ? multi.getKey(1) == null : key2.equals(multi.getKey(1))))
                {
                    it.remove();
                    modified = true;
                }
            }
            return modified;
        }

        /**
         * Removes all mappings where the first three keys are those specified.
         * <p>
         * This method removes all the mappings where the <code>MultiKey</code>
         * has three or more keys, and the first three match those specified.
         * 
         * @param key1  the first key
         * @param key2  the second key
         * @param key3  the third key
         * @return true if any elements were removed
         */
        public bool removeAll(Object key1, Object key2, Object key3)
        {
            bool modified = false;
            MapIterator it = mapIterator();
            while (it.hasNext())
            {
                MultiKey multi = (MultiKey)it.next();
                if (multi.size() >= 3 &&
                    (key1 == null ? multi.getKey(0) == null : key1.equals(multi.getKey(0))) &&
                    (key2 == null ? multi.getKey(1) == null : key2.equals(multi.getKey(1))) &&
                    (key3 == null ? multi.getKey(2) == null : key3.equals(multi.getKey(2))))
                {
                    it.remove();
                    modified = true;
                }
            }
            return modified;
        }

        /**
         * Removes all mappings where the first four keys are those specified.
         * <p>
         * This method removes all the mappings where the <code>MultiKey</code>
         * has four or more keys, and the first four match those specified.
         * 
         * @param key1  the first key
         * @param key2  the second key
         * @param key3  the third key
         * @param key4  the fourth key
         * @return true if any elements were removed
         */
        public bool removeAll(Object key1, Object key2, Object key3, Object key4)
        {
            bool modified = false;
            MapIterator it = mapIterator();
            while (it.hasNext())
            {
                MultiKey multi = (MultiKey)it.next();
                if (multi.size() >= 4 &&
                    (key1 == null ? multi.getKey(0) == null : key1.equals(multi.getKey(0))) &&
                    (key2 == null ? multi.getKey(1) == null : key2.equals(multi.getKey(1))) &&
                    (key3 == null ? multi.getKey(2) == null : key3.equals(multi.getKey(2))) &&
                    (key4 == null ? multi.getKey(3) == null : key4.equals(multi.getKey(3))))
                {
                    it.remove();
                    modified = true;
                }
            }
            return modified;
        }

        //-----------------------------------------------------------------------
        /**
         * Check to ensure that input keys are valid MultiKey objects.
         * 
         * @param key  the key to check
         */
        protected void checkKey(Object key)
        {
            if (key == null)
            {
                throw new java.lang.NullPointerException("Key must not be null");
            }
            if (key is MultiKey == false)
            {
                throw new java.lang.ClassCastException("Key must be a MultiKey");
            }
        }

        /**
         * Clones the map without cloning the keys or values.
         *
         * @return a shallow clone
         */
        public Object clone()
        {
            return new MultiKeyMap((AbstractHashedMap)map.clone());
        }

        /**
         * Puts the key and value into the map, where the key must be a non-null
         * MultiKey object.
         * 
         * @param key  the non-null MultiKey object
         * @param value  the value to store
         * @return the previous value for the key
         * @throws NullPointerException if the key is null
         * @throws ClassCastException if the key is not a MultiKey
         */
        public Object put(Object key, Object value)
        {
            checkKey(key);
            return map.put(key, value);
        }

        /**
         * Copies all of the keys and values from the specified map to this map.
         * Each key must be non-null and a MultiKey object.
         * 
         * @param mapToCopy  to this map
         * @throws NullPointerException if the mapToCopy or any key within is null
         * @throws ClassCastException if any key in mapToCopy is not a MultiKey
         */
        public void putAll(java.util.Map<Object, Object> mapToCopy)
        {
            for (java.util.Iterator<Object> it = mapToCopy.keySet().iterator(); it.hasNext(); )
            {
                Object key = it.next();
                checkKey(key);
            }
            map.putAll(mapToCopy);
        }

        //-----------------------------------------------------------------------
        public MapIterator mapIterator()
        {
            return map.mapIterator();
        }

        public int size()
        {
            return map.size();
        }

        public bool isEmpty()
        {
            return map.isEmpty();
        }

        public bool containsKey(Object key)
        {
            return map.containsKey(key);
        }

        public bool containsValue(Object value)
        {
            return map.containsValue(value);
        }

        public Object get(Object key)
        {
            return map.get(key);
        }

        public Object remove(Object key)
        {
            return map.remove(key);
        }

        public void clear()
        {
            map.clear();
        }

        public java.util.Set<Object> keySet()
        {
            return map.keySet();
        }

        public java.util.Collection<Object> values()
        {
            return map.values();
        }

        public java.util.Set<java.util.MapNS.Entry<Object,Object>> entrySet()
        {
            return (java.util.Set<java.util.MapNS.Entry<Object,Object>>)map.entrySet();
        }

        public override bool Equals(Object obj)
        {
            if (obj == this)
            {
                return true;
            }
            return map.equals(obj);
        }

        public override int GetHashCode()
        {
            return map.GetHashCode();
        }

        public override String ToString()
        {
            return map.toString();
        }

    }
}